/*
 * Zettelkasten - nach Luhmann
 * Copyright (C) 2001-2015 by Daniel Lüdecke (http://www.danielluedecke.de)
 * 
 * Homepage: http://zettelkasten.danielluedecke.de
 * 
 * 
 * This program is free software; you can redistribute it and/or modify it under the terms of the
 * GNU General Public License as published by the Free Software Foundation; either version 3 of 
 * the License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License along with this program;
 * if not, see <http://www.gnu.org/licenses/>.
 * 
 * 
 * Dieses Programm ist freie Software. Sie können es unter den Bedingungen der GNU
 * General Public License, wie von der Free Software Foundation veröffentlicht, weitergeben
 * und/oder modifizieren, entweder gemäß Version 3 der Lizenz oder (wenn Sie möchten)
 * jeder späteren Version.
 * 
 * Die Veröffentlichung dieses Programms erfolgt in der Hoffnung, daß es Ihnen von Nutzen sein 
 * wird, aber OHNE IRGENDEINE GARANTIE, sogar ohne die implizite Garantie der MARKTREIFE oder 
 * der VERWENDBARKEIT FÜR EINEN BESTIMMTEN ZWECK. Details finden Sie in der 
 * GNU General Public License.
 * 
 * Sie sollten ein Exemplar der GNU General Public License zusammen mit diesem Programm 
 * erhalten haben. Falls nicht, siehe <http://www.gnu.org/licenses/>.
 */
package de.danielluedecke.zettelkasten.database;

import de.danielluedecke.zettelkasten.util.Constants;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Level;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.IllegalAddException;
import org.jdom2.IllegalDataException;
import org.jdom2.IllegalNameException;

/**
 *
 * @author danielludecke
 */
public class Synonyms {

	/**
	 * XML Document that Stores the main data
	 */
	private Document synonymsFile;
	private boolean modified;
	/**
	 * XML document that is used as backup
	 */
	private Document backupdoc = null;

	public Synonyms() {
		clear();
	}

	/**
	 * Clears the XML document and creates a dummy-backup of the document, in case
	 * the original XML-document contains data.
	 */
	public final void clear() {
		// check whether backup document exists, whether autokorrektur-document exists
		// and whether
		// the autokorrektur-document has any data. only in this case we create a backup
		if (synonymsFile != null && synonymsFile.getRootElement().getContentSize() > 1) {
			// create new backup doc
			backupdoc = new Document(new Element("backup_synonyms"));
			// copy content
			backupdoc.getRootElement().addContent(synonymsFile.getRootElement().cloneContent());
		}
		synonymsFile = new Document(new Element("synonyms"));
		// reset modified state
		modified = false;
	}

	/**
	 * This method checks whether the XML document is ok or corrupted. in case there
	 * have been jdom-errors when adding new elements, the XML document
	 * {@link #synonymsFile} might be empty, while the backup-document
	 * {@link #backupdoc} has data. In this case, {@code false} is returned. If the
	 * XML-document is ok, {@code true} is returned.
	 *
	 * @return {@code true} if the main XML-document is ok, {@code false}if it might
	 *         be corrupted. <br>
	 *         <br>
	 *         You can use {@link #restoreDocument() restoreDocument()} to restore a
	 *         corrupted document.
	 */
	public boolean isDocumentOK() {
		// check whether we have any XML-document at all. proceed only, if we have no
		// document
		// of if the XML-document does not contain data
		if ((null == synonymsFile) || (synonymsFile.getRootElement().getContentSize() < 1)) {
			// now check whether we have a backup of the XML document, which has content
			if ((backupdoc != null) && (backupdoc.getRootElement().getContentSize() > 0)) {
				// if so, the backup contains data that the main document does not has
				// so, we assume the document is *not* ok
				return false;
			}
		}
		// else everything is fine
		return true;
	}

	/**
	 * In case we have a corrupted XML document with a backup document that has data
	 * (see {@link #isDocumentOK() isDocumentOK()}), we can restore the backupped
	 * data with this method.<br>
	 * <br>
	 * So, this method copies back the content of the {@link #backupdoc} to the
	 * original XML document {@link #synonymsFile}.
	 */
	public void restoreDocument() {
		// check whether we have a backup document that also contains data
		if ((backupdoc != null) && (backupdoc.getRootElement().getContentSize() > 1)) {
			// if we have it, create new main XML document
			synonymsFile = new Document(new Element("synonyms"));
			// and copy the content of the backup document to it
			synonymsFile.getRootElement().addContent(backupdoc.getRootElement().cloneContent());
		}
	}

	/**
	 * Sets the document, e.g. after loading the settings
	 *
	 * @param d the document with the synonyms-data
	 */
	public void setDocument(Document d) {
		if (d == null) {
			synonymsFile = new Document(new Element("synonyms"));
		} else {
			synonymsFile = d;
		}
		// check whether backup document exists, whether autokorrektur-document exists
		// and whether
		// the autokorrektur-document has any data. only in this case we create a backup
		if (synonymsFile.getRootElement().getContentSize() > 1) {
			// create new backup doc
			backupdoc = new Document(new Element("backup_synonyms"));
			// copy content
			backupdoc.getRootElement().addContent(synonymsFile.getRootElement().cloneContent());
		}
	}

	/**
	 * Gets the xml-document that contains the synonyms-data
	 *
	 * @return the xml-document with the synonyms-data
	 */
	public Document getDocument() {
		return synonymsFile;
	}

	/**
	 * This method returns the size of the xml data files
	 *
	 * @return the size of the data file
	 */
	public int getCount() {
		return synonymsFile.getRootElement().getContentSize();
	}

	/**
	 * Retrieves all synonyms - both index words and related synonyms - that
	 * currently are in the synonyms data base.
	 *
	 * @return all current synonyms as string array.
	 */
	public String[] getAllSynonyms() {
		LinkedList<String> synlist = new LinkedList<>();
		// iterate all synonyms elements
		for (int i = 0; i < getCount(); i++) {
			// retrieve synonyms
			String[] syns = getSynonymLine(i, true);
			// add to list
			for (String sy : syns) {
				// check for doubles
				if (!synlist.contains(sy)) {
					synlist.add(sy);
				}
			}
		}
		return synlist.toArray(new String[synlist.size()]);
	}

	public void appendSynonyms(Document syndoc) {
		int count = syndoc.getRootElement().getContentSize();
		for (int i = 0; i < count; i++) {
			String[] synline = getSynonymLine(syndoc, i, false);
			addSynonym(synline);
		}
	}

	public void addSynonym(String[] synline) {
		// we need at least two elements in the array: the original word and at least
		// one synonym
		if (null == synline || synline.length < 2) {
			return;
		}
		// if the synonyms-index-word already exists, don't add it...
		if (getSynonymPosition(synline[0]) != -1) {
			return;
		}
		// create new synonyms element
		Element synonym = new Element(Daten.ELEMENT_ENTRY);
		try {
			// trim spaces
			synline[0] = synline[0].trim();
			// set the original word as value-attribute to the "entry"-element
			synonym.setAttribute("indexword", synline[0]);
			// now go through the rest of the string-array
			for (int cnt = 1; cnt < synline.length; cnt++) {
				// create a sub-child "syn" for each further synonym
				Element syn = new Element("syn");
				// set text from string array
				syn.setText(synline[cnt].trim());
				// add child to synonym-element
				synonym.addContent(syn);
			}
			// finally, add new element to the document
			synonymsFile.getRootElement().addContent(synonym);
			setModified(true);
		} catch (IllegalNameException | IllegalDataException | IllegalAddException ex) {
			Constants.zknlogger.log(Level.SEVERE, ex.getLocalizedMessage());
		}
	}

	public String[] getSynonymLine(Document doc, int nr, boolean matchcase) {
		// get element
		Element syn = retrieveElement(doc, nr);
		// init return value
		String[] retval = null;
		// if we have a valid element, go on
		if (syn != null) {
			// get list of child-element with synonyms
			List<?> l = syn.getChildren();
			// create array
			retval = new String[l.size() + 1];
			// first element of the array is the index word
			// retrieve indexword-attribute
			String attr = syn.getAttributeValue("indexword");
			// check whether value ok
			if (attr != null) {
				// then use it for array
				if (matchcase) {
					retval[0] = attr;
				} else {
					retval[0] = attr.toLowerCase();
				}
			} else {
				// else log error message, telling the number of the corrupted synonym-data
				Constants.zknlogger.log(Level.WARNING, "No index word for synonym {0} found.", String.valueOf(nr));
				// and return null
				return null;
			}
			// following elements are the synonyms. therefore, copy the children's text to
			// the array
			for (int cnt = 0; cnt < l.size(); cnt++) {
				// get the element
				Element e = (Element) l.get(cnt);
				// get the element's text
				if (matchcase) {
					retval[cnt + 1] = e.getText();
				} else {
					retval[cnt + 1] = e.getText().toLowerCase();
				}
			}
		}
		return retval;
	}

	/**
	 * This method returns a synonym (as index-word) with its related synonyms. The
	 * return-value is a string-array with the first element being the index-word,
	 * and the following elements being the related synonyms.
	 *
	 * @param nr        the number of the requested synonym, with a range from 0 to
	 *                  {@link #getCount()}-1
	 * @param matchcase use {@code true} if the synonyms (strings in the return
	 *                  array) should be returned in original case-letters. use
	 *                  {@code false} if they should be returned in
	 *                  lower-case-letters
	 * @return a string-array with the first element being the index-word, and the
	 *         following elements being the related synonyms; or {@code null}, if no
	 *         synonym was found
	 */
	public String[] getSynonymLine(int nr, boolean matchcase) {
		return getSynonymLine(synonymsFile, nr, matchcase);
	}

	/**
	 * This method sets a new synonm-line, i.e. a synonym (as index-word) with its
	 * related synonyms. The new synonyms have to passed as string-parameter
	 * {@code synline}.
	 *
	 * @param nr      the number of the requested synonym, with a range from 0 to
	 *                (getCount()-1)
	 * @param synline a string-array with the first element being the index-word,
	 *                and the following elements being the related synonyms
	 */
	public void setSynonymLine(int nr, String[] synline) {
		// get element
		Element synonym = retrieveElement(nr);
		// remove all child-content (i.e. all synonyms)
		synonym.removeContent();
		try {
			// set the original word as value-attribute to the "entry"-element
			synonym.setAttribute("indexword", synline[0]);
			// now go through the rest of the string-array
			for (int cnt = 1; cnt < synline.length; cnt++) {
				// create a sub-child "syn" for each further synonym
				Element syn = new Element("syn");
				// set text from string array
				syn.setText(synline[cnt]);
				// add child to synonym-element
				synonym.addContent(syn);
				setModified(true);
			}
		} catch (IllegalDataException | IllegalNameException ex) {
			Constants.zknlogger.log(Level.SEVERE, ex.getLocalizedMessage());
		}
	}

	public boolean mergeSynonymLines(String originalLineIndexWord, String mergedLineIndexWord) {
		return mergeSynonymLines(getSynonymPosition(originalLineIndexWord), getSynonymPosition(mergedLineIndexWord));
	}

	/**
	 * This method merges two synonym-lines, where the line given by the
	 * index-number {@code originalLine} remains, while the line with the
	 * index-number {@code mergedLine} will be appended to the synonyms of the
	 * {@code originalLine}.
	 *
	 * @param originalLine the index-number of the original line that should remain
	 *                     in the database
	 * @param mergedLine   the index-number of the synonym-line that should be
	 *                     appended to the original line.
	 * @return
	 */
	public boolean mergeSynonymLines(int originalLine, int mergedLine) {
		// check for valid parameters
		if (originalLine < 0 || originalLine >= getCount() || mergedLine < 0 || mergedLine >= getCount()) {
			return false;
		}
		// retrieve original synonyms line
		String[] oriline = getSynonymLine(originalLine, true);
		// retrieve apped-synonyms line
		String[] appendline = getSynonymLine(mergedLine, true);
		// check for valid values
		if (null == oriline || null == appendline) {
			return false;
		}
		// create array-list that will contain new synonym line
		List<String> newline = new ArrayList<>();
		// add all "old", original synonyms
		newline.addAll(Arrays.asList(oriline));
		// now append all "merged" synonyms
		newline.addAll(Arrays.asList(appendline));
		// set new line
		setSynonymLine(originalLine, newline.toArray(new String[newline.size()]));
		// remove old synonym-line
		synonymsFile.getRootElement().removeContent(mergedLine);
		setModified(true);
		return true;
	}

	/**
	 * This method sets a new index-word to the synonyms-line with the number
	 * {@code nr}.
	 *
	 * @param nr        the number of the synonyms-line, where the index-word should
	 *                  be changed
	 * @param indexword the new index-word, as string
	 */
	public void setIndexWord(int nr, String indexword) {
		// get element
		Element synonym = retrieveElement(nr);
		// set the original word as value-attribute to the "entry"-element
		synonym.setAttribute("indexword", indexword);
		setModified(true);
	}

	/**
	 * This method adds a new synonym {@code appendsyn} to an existing synonym-line
	 * {@code nr}.
	 *
	 * @param nr        the number of the existing synonym-line
	 * @param appendsyn the new synonym that should be appended to that line.
	 */
	public void appendSingleSynonym(int nr, String appendsyn) {
		// get element
		Element synonym = retrieveElement(nr);
		// chekc for valid value
		if (synonym != null) {
			try {
				// create a sub-child "syn" for each further synonym
				Element syn = new Element("syn");
				// set text from string array
				syn.setText(appendsyn);
				// add child to synonym-element
				synonym.addContent(syn);
				setModified(true);
			} catch (IllegalAddException | IllegalDataException ex) {
				Constants.zknlogger.log(Level.SEVERE, ex.getLocalizedMessage());
			}
		}
	}

	/**
	 * This method returns a synonym (as index-word) with its related synonyms. The
	 * return-value is a string-array with the first element being the index-word,
	 * and the following elements being the related synonyms. <br>
	 * <br>
	 * If a complete synonyme-line from <b>any word</b> is requested, use
	 * {@link #getSynonymLineFromAny(java.lang.String) #getSynonymLineFromAny()}
	 *
	 * @param indexword string-value of the synonym (original- or index-word) which
	 *                  is searched for in the list
	 * @param matchcase
	 * @return a string-array with the first element being the index-word, and the
	 *         following elements being the related synonyms; or {@code null}, if no
	 *         synonym was found
	 */
	public String[] getSynonymLine(String indexword, boolean matchcase) {
		// retrieve position of the index-word
		int pos = getSynonymPosition(indexword);
		// if we found an index-word, return the synonyms, else return null
		if (pos != -1) {
			return getSynonymLine(pos, matchcase);
		} else {
			return null;
		}
	}

	private Element retrieveElement(Document doc, int pos) {
		// create a list of all elements from the given xml file
		try {
			List<?> elementList = doc.getRootElement().getContent();
			// and return the requestet Element
			try {
				return (Element) elementList.get(pos);
			} catch (IndexOutOfBoundsException e) {
				return null;
			}
		} catch (IllegalStateException e) {
			Constants.zknlogger.log(Level.WARNING, e.getLocalizedMessage());
			return null;
		}
	}

	/**
	 * This function retrieves an element of a xml document at a given position.
	 * used for other methods like getAuthor or getKeyword. The position is a value
	 * from 0 to (size-1).
	 *
	 * @param pos the position of the element, ranged from 0 to (document-size - 1)
	 * @return the element if a match was found, otherwise null
	 */
	private Element retrieveElement(int pos) {
		return retrieveElement(synonymsFile, pos);
	}

	/**
	 * This method returns the position of a synonym (original or index word) in the
	 * XML file if the synonym doesn't exist, the return value is -1. <br>
	 * <br>
	 *
	 * @param indexword string-value of the synonym (original- or index-word) which
	 *                  is searched for in the list
	 * @return the position of the synonym or -1 if no match was found
	 */
	public int getSynonymPosition(String indexword) {
		// trim spaces
		indexword = indexword.trim();
		// create a list of all author elements from the author xml file
		try {
			List<?> synList = synonymsFile.getRootElement().getContent();
			// and an iterator for the loop below
			Iterator<?> iterator = synList.iterator();
			// counter for the return value if a found synonym matches the parameter
			int cnt = 0;

			while (iterator.hasNext()) {
				Element syn = (Element) iterator.next();
				// if synonym-index-word matches the parameter string, return the position
				if (indexword.equalsIgnoreCase(syn.getAttributeValue("indexword"))) {
					return cnt;
				}
				// else increase counter
				cnt++;
			}
			// if no author was found, return -1
			return -1;
		} catch (IllegalStateException e) {
			Constants.zknlogger.log(Level.WARNING, e.getLocalizedMessage());
			return -1;
		}
	}

	/**
	 * This method returns the position of a synonym in the XML file, independent
	 * from wether the requested word is an index-word or a related synonym. if the
	 * synonym doesn't exist, the return value is -1. <br>
	 * <br>
	 * If the position of a certain index-word only is requestes, use
	 * {@link #getSynonymPosition(java.lang.String)
	 * getSynonymPosition(java.lang.String)} instead. <br>
	 * <br>
	 * If a complete synonyme-line is requested, use
	 * {@link #getSynonymLine(java.lang.String) #getSynonymLine(java.lang.String)}
	 * or {@link #getSynonymLineFromAny(java.lang.String)
	 * getSynonymLineFromAny(java.lang.String)}.
	 *
	 * @param synonym   string-value of the synonym (wither original- or index-word,
	 *                  or any related synonym) which is searched for in the list.
	 * @param matchcase
	 * @return the position of the synonym or -1 if no match was found
	 */
	public int findSynonym(String synonym, boolean matchcase) {
		// go through all entries
		for (int cnt = 0; cnt < getCount(); cnt++) {
			// get each synonym-line
			String[] synline = getSynonymLine(cnt, matchcase);
			// check for valid value
			if (synline != null) {
				// if the array contains the requested synonym, return line-position
				for (String s : synline) {
					if (s.equalsIgnoreCase(synonym)) {
						return cnt;
					}
				}
			}
		}
		return -1;
	}

	/**
	 * This method searches all index-words of the synonyms-file and compares them
	 * to the parameter {@code synonym}. If a match was found, i.e. if the parameter
	 * {@code synonym} is an index-word, {@code true} is returned, {@code false}
	 * otherwise.
	 *
	 * @param synonym   string-value which should be checked whether it is an
	 *                  index-word or not
	 * @param matchcase
	 * @return {@code true} if the parameter {@code synonym} is an index-word,
	 *         {@code false} otherwise
	 */
	public boolean isIndexWord(String synonym, boolean matchcase) {
		// go through all entries
		for (int cnt = 0; cnt < getCount(); cnt++) {
			// get each synonym-line
			String[] synline = getSynonymLine(cnt, matchcase);
			// if the index-word of the array contains the requested synonym, return true
			if (synline != null && synline[0].equalsIgnoreCase(synonym)) {
				return true;
			}
		}
		return false;
	}

	/**
	 * This method searches through all synonyms in the data-file and returns the
	 * first index-word when a related synonym of that index-word matches
	 * {@code synvalue}.<br>
	 * <br>
	 * <b>Note:</b> This method excludes index-words from the search! Only
	 * non-index-words of each synonym-line are checked for matching the parameter
	 * {@code synvalue}.
	 *
	 * @param synvalue  the synonym where we want to retrieve the related index-word
	 * @param matchcase whether the check for the synonyms should be case sensitive
	 *                  {@code true} or not.
	 * @return the index-word of the related synonym {@code synvalue}, or
	 *         {@code null} if no index-word was found.
	 */
	public String getIndexWord(String synvalue, boolean matchcase) {
		// go through all entries
		for (int cnt = 0; cnt < getCount(); cnt++) {
			// get each synonym-line
			String[] synline = getSynonymLine(cnt, matchcase);
			// go through array to find the value "synvalue"...
			if (synline != null && synline.length > 1) {
				for (int c = 1; c < synline.length; c++) {
					// if we found the synvalue, return the related indexword
					if (synline[c].equalsIgnoreCase(synvalue)) {
						return synline[0];
					}
				}
			}
		}
		// else return null...
		return null;
	}

	/**
	 * This method returns the related synonyms of a given synonym in the XML file,
	 * independent from wether the requested word is an index-word or a related
	 * synonym. if the synonym doesn't exist, the return value is null. <br>
	 * <br>
	 * If a complete synonyme-line from the <b>index-word</b> only is requested, use
	 * {@link #getSynonymLine(java.lang.String) #getSynonymLine(java.lang.String)}
	 *
	 * @param synonym   string-value of the synonym (wither original- or index-word,
	 *                  or any related synonym) which is searched for in the list.
	 * @param matchcase
	 * @return the other related synonyms as string array, with the parameter
	 *         {@code synonym} <b>included</b>, or null if {@code synonym} wasn't
	 *         found.
	 */
	public String[] getSynonymLineFromAny(String synonym, boolean matchcase) {
		// go through all entries
		for (int cnt = 0; cnt < getCount(); cnt++) {
			// get each synonym-line
			String[] synline = getSynonymLine(cnt, matchcase);
			// check for valid value
			if (synline != null) {
				// if the array contains the requested synonym, return the synonym-line
				// (string-array)
				for (String s : synline) {
					if (s.equalsIgnoreCase(synonym)) {
						return synline;
					}
				}
			}
		}
		return null;
	}

	/**
	 * sets the modified state of the bookmark-data
	 *
	 * @param m true when the bookmark-data was modified, or false if modifications
	 *          were saved.
	 */
	public void setModified(boolean m) {
		modified = m;
	}

	/**
	 * returns the modified state of the bookmark-data
	 *
	 * @return {@code true} if the bookmark-data was modified, false if it's
	 *         unchanged
	 */
	public boolean isModified() {
		return modified;
	}
}
